﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net;
using SharpSvn;
using System.IO;

namespace PreCommitHook
{
    public class PreCommitValidator
    {
        private readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private readonly ISvnRepository repository = new SvnRepository();

        public PreCommitResult ShouldProceedWithCommit(string svnRepositoryPath, string svnTransaction)
        {

            try
            {
                ISVNChangeInfo revisionInfo = this.repository.GetSvnRevisionInfo(svnTransaction, svnRepositoryPath);
               
                if (String.IsNullOrEmpty( revisionInfo.LogMessage ))
                {
                    log.Info(String.Format("ShouldProceedWithCommit: trying to commit with log message: '{0}'", revisionInfo.LogMessage));
                    return new PreCommitResult(false, "Log message cannot be empty string or null.");
                }

                string errorMessage = "This commit cannot be proceed because it doesnt follow the commit policy.\n";
                if (this.ShouldBeFileIgnored(revisionInfo.ChangedItems, revisionInfo.Author, ref errorMessage) == true)
                    return new PreCommitResult(false, errorMessage);

            }
            catch (Exception e)
            {
                log.Fatal(string.Format("Error in validation of commit status for path: {0}, transactionId: {1}. error message: {2}; exception stack trace: {3}", svnTransaction, svnRepositoryPath, e.Message, e.StackTrace));

                return new PreCommitResult(false, "Application exception occurred. " + e.Message);
            }

            return new PreCommitResult(true, string.Empty);
        }

        /// <summary>
        /// ignore paths and file types which are marked for ignoring in config.xml and validate the code coverage for every input file
        /// </summary>
        /// <param name="changedPaths"></param>
        /// <param name="author"></param>
        /// <returns></returns>
        private bool ShouldBeFileIgnored(IEnumerable<SVNItem> changedPaths, string author, ref string errorMessage)
        {
            bool isCodeCoverageDataAssembled = false; //if more than one file is committed, run the code coverage once only, rather than
            //running it for every file

            //run test, generate code coverage results here        
            foreach (SVNItem item in changedPaths)
            {
                if (item.Action == SvnChangeAction.Delete)
                {
                    log.Info(String.Format("ShouldBeFileIgnored method continues with the other items because of the committing action DELETE, path: {0}", item.Path));
                    continue;
                }
                if (item.Type == SvnNodeKind.Directory)
                {
                    if (!ContentPolicyManager.AllowContentCommit(item.Path, false))
                    {
                        errorMessage += String.Format("The folder '{0}' doesn't meet the commit policy from fileCommitedFilesPolicy.xml.\n Exclude the folder from your commit list and try again.", "");
                        return true;
                    }
                    log.Info(String.Format("ShouldBeFileIgnored method continues with the other items because of type folder, path: {0}", item.Path));
                    continue;
                }

                if (item.Type == SvnNodeKind.File)
                {
                    
                    log.Info(String.Format("ShouldBeFileIgnored method with action:{0}, path:{1}, author: {2},  type: {3}.", item.Action, item.Path, author, item.Type));
                    string fileToCommit = AssemblyHelper.GetFileNameFromPath(item.Path);

                    //check if the item which is committed follows content policy
                    if (!ContentPolicyManager.AllowContentCommit(fileToCommit, true))
                    {
                        errorMessage += String.Format("The file '{0}' doesn't meet the commit policy from fileCommitedFilesPolicy.xml.\n Exclude the file from your commit list and try again.", fileToCommit);
                        return true;
                    }

                    //if the file is not .cs file continue, we dont care about other files
                    if (!AssemblyHelper.IsCodeFile(fileToCommit))
                    {
                        log.Info(String.Format("ShouldBeFileIgnored method continues with the other items because of file type (not code file/cs), path: {0}", item.Path));
                        continue;
                    }

                    MSCodeCoverageParser msParser = new MSCodeCoverageParser();
                    
                    //if the user who commits the file should not be checked for code coverage according the config file we return false
                    SubMethodResult shouldUserBeChecked = msParser.ShouldCheckConfigUsers(author);
                    if (shouldUserBeChecked == SubMethodResult.BreakTrue)
                        return false;
                    

                    string repositoryPath = System.Configuration.ConfigurationSettings.AppSettings["RepositoryPath"];
                    string projectsPath = System.Configuration.ConfigurationSettings.AppSettings["ProjectsFiles"];
                    projectsPath = projectsPath + @"\" + author;
                    SvnRepository svnRepositoryManager = new SvnRepository();

                    bool isTestAssembly = false;
                    string assemblyName = svnRepositoryManager.GetAssemblyByFile(fileToCommit, projectsPath, item.Path, ref isTestAssembly); //svnRepositoryManager.GetFileAssembly(repositoryPath, fileToCommit); 
                    
                    //we dont check for code coverage the unit tests assemblies
                    if (true == isTestAssembly)
                    {
                        log.Info(String.Format("{0} is the assembly with unit tests and it shouldnt be checked for code coverage", assemblyName));
                        continue;
                    }
                    
                    if (fileToCommit == "AssemblyInfo.cs" && item.Path.Contains("Properties/AssemblyInfo.cs"))
                    {
                        log.Info(String.Format("{0} should not be checked and the method continues with the other items.", fileToCommit));
                        continue;
                    }

                    if (String.IsNullOrEmpty(assemblyName))
                    {
                        
                        errorMessage += String.Format("Cannot fetch the assembly name for the file '{0}'. No code coverage can be run.", fileToCommit);
                        return true;
                        
                    }

                    if (false == msParser.ShouldCheckAssembly(assemblyName))
                    {
                        log.Info(String.Format("Assembly {0} should not be checked for code coverage according settings xml", assemblyName));
                        continue;
                    }
                    //execute one code coverage extraction and test runs and validate against them
                    log.Info(String.Format("Perform code coverage checking for file '{0}' in assembly '{1}'", fileToCommit, assemblyName));
                   
                    //Code coverage analysis is run here
                    //string userNameWhoCommits = System.Configuration.ConfigurationSettings.AppSettings["SVNUserNameWhoCommits"];

                    if (false == isCodeCoverageDataAssembled)
                    {
                        CodeCoverageManager manager = new CodeCoverageManager(author);

                        string[] assembls = new string[] { };
                        string[] tests = new string[] { };
                        manager.GetAssembliesToCheck(author, ref assembls, ref tests);

                       // foreach (var a in assembls)
                       //     log.Info(String.Format("Assembly {0} will be tested", a));

                       
                        manager.Assemblies = assembls;
                        manager.TestAssemblies = tests;
                       
                       // foreach (var t in manager.TestAssemblies)
                       //     log.Info(String.Format("Test assembly {0}", t));

                        // vsintr -coverage myassembly.exe
                        manager.InstrumentAssemblies();

                        // start vsperfmon -coverage -output:mytestrun.coverage
                        manager.StartRunCoverage();

                        // mstest.exe
                        manager.ExerciseAssemblies();

                        // vsperfcmd -shutdown
                        manager.FinishRunCoverage();

                        // write to xml
                        manager.WriteToXml();

                        isCodeCoverageDataAssembled = true;
                    }

                    msParser.UnitTestResultsFilePath = CodeCoverageManager.CodeCoverageFolderPath + @"\TestCoverage.trx";
                    var result = msParser.AreAllTestCasesSuccessful();

                    if (false == result)
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.AppendLine("Some of the unit tests didn't run successfully.");
                        errorMessage += "Some of the unit tests didn't run successfully and the commit is rolled back";
                        sb.AppendLine(String.Format("The user who was trying to commit:{0}", author));
                        sb.AppendLine();
                        foreach (var failedTest in msParser.FailedTestsList)
                        {
                            log.Info(String.Format("Failed test details: {0}", failedTest.ToString()));
                            sb.AppendLine(failedTest.ToString());
                            errorMessage += String.Format("Failed test details: {0}.\n", failedTest.ToString());
                        }
                        EmailNotifier.SendEmail(sb.ToString());
                        return true;
                    }

                    msParser.CoverageFilePath = CodeCoverageManager.CodeCoverageFolderPath + @"\CoverageResult.xml";
                    string csFileWithoutExtension = AssemblyHelper.GetCSFileWithoutExtension(fileToCommit);
                    bool checkResult = msParser.CheckTrashHoldCoverage(assemblyName, csFileWithoutExtension , author);
                    if (false == checkResult)
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.AppendLine("The committed file doesn't follow the code coverage policy.");
                        sb.AppendLine(String.Format("Code coverage details:\n file name:'{0}', Assembly: '{1}', user who is trying to commit: '{2}'", csFileWithoutExtension, assemblyName, author)); //pass these arguments from svn hook arguments
                        sb.AppendLine("Check the hook's log file for details.");
                        EmailNotifier.SendEmail(sb.ToString()); 
                        errorMessage += String.Format("Assembly {0} or file {1} doesn't follow the commit policy and their code coverage is below the given threshold in codeCoverageSettings.xml.", assemblyName, csFileWithoutExtension);
                        return true;
                    }
                }
            }
            return false;
        }
    }
}
